package com.gitee.wsl.net.client

import com.gitee.wsl.ext.base.currentTimeMillis
import com.gitee.wsl.flow.create.reload.ReloadStateFlow
import com.gitee.wsl.flow.create.reload.ReloadStateFlowImpl
import com.gitee.wsl.flow.create.reload.RequestState
import com.gitee.wsl.net.backoff
import com.gitee.wsl.net.base.XUbiquache
import com.gitee.wsl.net.expiresDate
import com.gitee.wsl.net.nextRefreshDate
import com.gitee.wsl.net.serverDate
import com.gitee.wsl.net.throwIfNotSuccessful
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.statement.HttpResponse
import io.ktor.client.utils.CacheControl
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.util.reflect.TypeInfo
import io.ktor.util.reflect.typeInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.io.IOException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.cancellation.CancellationException


inline fun <reified T> HttpClient.reloadStateFlow(
    context: CoroutineContext = Dispatchers.Default,
    defaultRefreshInterval: Long? = null,
    defaultRefreshBackoff: Long = DEFAULT_REFRESH_BACKOFF,
    cancellationTimeout: Long = CANCELLATION_DELAY,
    builder: HttpRequestBuilder
) = ktorStateFlow<T>(
    context,
    defaultRefreshInterval,
    defaultRefreshBackoff,
    cancellationTimeout){
        request(builder.apply { method = HttpMethod.Get })
    }

const val DEFAULT_REFRESH_BACKOFF = 5 * 1000L
const val CANCELLATION_DELAY = 2000L

/**
 * Builds a StateFlow that has values yielded from the given [request] that executes on a scope tied to the StateFlow observer cycle.
 *
 * The [request] block starts executing when the returned [StateFlow] becomes active.
 * If the [StateFlow] becomes inactive while the [request] block is executing, it
 * will be cancelled after [cancellationTimeout] milliseconds unless the [StateFlow] becomes active again
 * before that timeout (to gracefully handle cases like Activity rotation).
 *
 * After a cancellation, if the [StateFlow] becomes active again, the [request] block will be re-executed
 * from the beginning.
 *
 * @param context The CoroutineContext to run the given request block in. Defaults to [Dispatchers.IO].
 * @param defaultRefreshInterval The fallback refresh interval in millis, used if nothing is specified by the response headers.
 * 			If there is no interval specified by the response headers nor as a fallback, the request is treated as one-shot.
 * @param defaultRefreshBackoff The fallback minimum refresh delay in millis, used if nothing is specified by the response headers.
 * 			Note that `defaultRefreshInterval` takes precedence over this, if neither is specified by the response headers.
 * @param cancellationTimeout The timeout in millis before cancelling the request block if there are no active collectors.
 * @param request The block to run to execute the request when the [StateFlow] has active observers.
 */
inline fun <reified T> ktorStateFlow(
    context: CoroutineContext = Dispatchers.Default,
    defaultRefreshInterval: Long? = null,
    defaultRefreshBackoff: Long = DEFAULT_REFRESH_BACKOFF,
    cancellationTimeout: Long = CANCELLATION_DELAY,
    noinline request: suspend (cacheControl: String?) -> HttpResponse,
): ReloadStateFlow<RequestState<T>> =
    ktorStateFlow(
        typeInfo<T>(),
        context,
        defaultRefreshInterval,
        defaultRefreshBackoff,
        cancellationTimeout,
        request
    )

@PublishedApi
internal fun <T> ktorStateFlow(
    typeInfo: TypeInfo,
    context: CoroutineContext,
    defaultRefreshInterval: Long?,
    defaultRefreshBackoff: Long,
    cancellationTimeout: Long,
    request: suspend (cacheControl: String?) -> HttpResponse,
): ReloadStateFlow<RequestState<T>> =
    ReloadStateFlowImpl(context, cancellationTimeout, MutableStateFlow(RequestState.Loading)) { forceFresh ->
        try {
            emit(RequestState.Loading)
            var repeat = true
            if (forceFresh) {
                repeat = loadAndWait(request, typeInfo, CacheControl.NO_CACHE, defaultRefreshInterval, defaultRefreshBackoff)
            } else {
                try {
                    loadAndWait(request, typeInfo, CacheControl.ONLY_IF_CACHED, 0, 0)
                } catch (e: ResponseException) {
                    // ignore, continue
                } catch (e: IOException) {
                    //youtrack.jetbrains.com/issue/KT-7128
                }
            }
            while (repeat) {
                repeat = loadAndWait(request, typeInfo, null, defaultRefreshInterval, defaultRefreshBackoff)
            }
        } catch (e: CancellationException) {
            // ignore
        } catch (e: Exception) {
            emit(RequestState.Error(e) {
                forceReload()
            })
        }
    }

private suspend fun <T> ReloadStateFlowImpl<RequestState<T>>.loadAndWait(
    request: suspend (cacheControl: String?) -> HttpResponse,
    typeInfo: TypeInfo,
    cacheControl: String?,
    defaultRefreshInterval: Long?,
    defaultRefreshBackoff: Long,
): Boolean {
    val response = request(cacheControl)
    if (cacheControl == CacheControl.ONLY_IF_CACHED) {
        if (response.headers[HttpHeaders.XUbiquache]?.contains(CacheControl.ONLY_IF_CACHED) != true) {
            throw IllegalStateException("KtorStateFlow requires the Ubiquache plugin to be installed")
        }
    }
    response.throwIfNotSuccessful()
    emit(RequestState.Result(response.body(typeInfo)))
    val nextRefresh = response.nextRefreshDate() ?: response.expiresDate()
    val backoff = response.backoff()?.coerceAtLeast(0) ?: defaultRefreshBackoff
    val timeMillis = currentTimeMillis
    val minDelay = if (cacheControl == CacheControl.ONLY_IF_CACHED) {
        (backoff - (timeMillis - (response.serverDate() ?: timeMillis))).coerceAtLeast(0)
    } else {
        backoff
    }
    val delay = nextRefresh?.minus(timeMillis)?.coerceAtLeast(minDelay) ?: defaultRefreshInterval
    delay?.let { delay(it) }
    return delay != null
}